﻿using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Collections;
using System.Security.Cryptography;

namespace NntpSharp
{
    /// <summary>
    /// Class that handles NNTP communication.
    /// </summary>
    public class NntpSharpLib : System.Net.Sockets.TcpClient
    {
        /// <summary>
        /// Exception class for NntpSharpLib
        /// </summary>
        public class NntpException : System.ApplicationException
        {
            private int ERRORCODE = 0;

            /// <summary>
            /// Exception method for NntpSharpLib
            /// </summary>
            /// <param name="str">String. Response string from 
            /// the nntp server.</param>
            public NntpException(string str)

                : base(str)
            {

            }
            /// <summary>
            /// Exception method that takes both the error code
            /// and the error string.
            /// </summary>
            /// <param name="code">Response code given by the
            /// nntp server.</param>
            /// <param name="str">String. Reponse string from
            /// the nntp server.</param>
            public NntpException(int code, string str)
                : base(str)
            {
                this.ERRORCODE = code;
            }

            /// <summary>
            /// Method to report the server code from the Nntp
            /// server.
            /// </summary>
            public int Code
            {
                get { return this.ERRORCODE; }
            }

        };

        //============== GLOBAL VARIABLES ==============//
        private string SERVERMESSAGE = string.Empty;
        //-------------- GLOBAL VARIALBES --------------//

        /// <summary>
        /// Print out the message received from the server.
        /// Good for logging and debugging.
        /// </summary>
        /// <returns>Message received from server</returns>
        public string ServerMessage
        {
            get { return SERVERMESSAGE; }
        }

        /// <summary>
        /// Just initializes the timeout for our
        /// communications.
        /// </summary>
        public NntpSharpLib()
        {
            base.SendTimeout = 10 * 1000;
        }

        // Below is not needed - was an old Connect method. Keeping it
        // around in case I break something and need it.

        //public bool Connect(string server)
        //{
        //    bool retVal = false;
        //    string response = string.Empty;

        //    try
        //    {
        //        base.Connect(server, 119);

        //        response = Response();
        //        this.SERVERMESSAGE = response;
        //        System.Console.WriteLine(this.SERVERMESSAGE);

        //        if (response.Substring(0, 3) != "200")
        //        {
        //            throw new NntpException(response);
        //        }
        //        else
        //            retVal = true;
        //    }
        //    catch (Exception e)
        //    {
        //        System.Console.WriteLine("Exception in Connect");
        //        System.Console.WriteLine(e.Message);
        //        //throw new NntpException(e.Message);
        //    }

        //    return retVal;
        //}

        /// <summary>
        /// Connect to the server
        /// </summary>
        /// <param name="server">String value of hostname</param>
        /// <param name="port">Int value of port</param>
        /// <returns>True if successful</returns>
        public new bool Connect(string server, int port)
        {
            bool retVal = false;

            string response = string.Empty;
            string code = string.Empty;

            if (port < 1)
            {
                throw new NntpException(1, "Invalid port");
            }

            Debug.WriteLine("Server: " + server + " | port: " + port.ToString());
            Console.WriteLine("Server: " + server + " | port: " + port.ToString());

            try
            {
                base.Connect(server, port);

                response = Response();
                this.SERVERMESSAGE = response;

                Debug.WriteLine(response);
                Console.WriteLine(response);
                code = response.Substring(0, 3);

                if (code != "200")
                {
                    throw new NntpException(System.Int32.Parse(code), response);
                }
                else
                {
                    retVal = true;
                }
            }
            catch (System.Net.Sockets.SocketException se)
            {
                System.Console.WriteLine("Exception in Connect");
                System.Console.WriteLine(se.Message);
                this.SERVERMESSAGE = se.Message;
                throw se;
                //throw new NntpException(se.Message);
            }
            catch (System.NotSupportedException nse)
            {
                System.Console.WriteLine("Exception in Connect");
                System.Console.WriteLine(nse.Message);
                this.SERVERMESSAGE = nse.Message;
                throw nse;
                //throw new NntpException(nse.Message);
            }
            catch (System.ObjectDisposedException ode)
            {
                System.Console.WriteLine("Exception in Connect");
                System.Console.WriteLine(ode.Message);
                this.SERVERMESSAGE = ode.Message;
                throw ode;
                //throw new NntpException(ode.Message);
            }
            catch (System.ArgumentNullException ane)
            {
                System.Console.WriteLine("Exception in Connect");
                System.Console.WriteLine(ane.Message);
                this.SERVERMESSAGE = ane.Message;
                throw ane;
                //throw new NntpException(ane.Message);
            }
            catch (System.Security.SecurityException se)
            {
                System.Console.WriteLine("Exception in Connect");
                System.Console.WriteLine(se.Message);
                this.SERVERMESSAGE = se.Message;
                throw se;
                //throw new NntpException(se.Message);
            }
            catch (System.Exception e)
            {
                System.Console.WriteLine("Exception in Connect");
                throw new NntpException(e.Message);
            }

            return retVal;
        }

        /// <summary>
        /// Authenticates with the news server
        /// </summary>
        /// <param name="username">Username for news server</param>
        /// <param name="password">Password for news server</param>
        public void Authenticate(string username, string password)
        {
            string message = "AUTHINFO USER " + username + "\r\n";
            string response = string.Empty;
            string code = string.Empty;

            try
            {
                Write(message);
                response = Response();
                System.Console.WriteLine("AUTHINFO response: " + response);
                code = response.Substring(0, 3);

                switch (code)
                {
                    //If message code received is 281,
                    //then we have successfully authenticated
                    case "281":
                        return;
                    //If message code received is 381,
                    //then a password is required
                    case "381":
                        message = "AUTHINFO PASS " + password;
                        Write(message);
                        response = Response();
                        code = response.Substring(0, 3);
                        switch (code)
                        {


                            //If message code received is 281,
                            //then we have successfully authenticated
                            case "281":
                                return;
                            case "481":
                            default:
                                throw new NntpException(response);
                        }
                    //If message code received is 481,
                    //then authentication failed/rejected
                    case "481":
                    default:
                        throw new NntpException(response);
                }
            }
            catch (Exception e)
            {
                System.Console.WriteLine("Caught exception in Authenticate!");
                System.Console.WriteLine(e.Message);
                throw e;
            }
        }

        /// <summary>
        /// Gracefully disconnects from server.
        /// </summary>
        public void Disconnect()
        {
            string message = "QUIT\r\n";
            string response = string.Empty;

            try
            {
                Write(message);
                response = Response();

                if (response.Substring(0, 3) != "205")
                {
                    throw new NntpException(response);
                }
                else
                {
                    System.Console.WriteLine(response);
                    this.SERVERMESSAGE = response;
                }
            }
            catch (System.Exception e)
            {
                System.Console.WriteLine("Exception in Disconnect");
                throw new NntpException(e.Message);
            }
        }

        /// <summary>
        /// Gets a list of newsgroups available in the news server
        /// </summary>
        /// <returns>List of type string of newsgroups</returns>
        public List<string> GetNewsgroups()
        {
            string message = string.Empty;
            string response = string.Empty;
            List<string> retval = new List<string>();
            message = "LIST\r\n";

            try
            {
                Write(message);
                response = Response();

                if (response.Substring(0, 3) != "215")
                {
                    throw new NntpException(response);
                }

                //if (code != "215")
                //{
                //    if (code == "480")
                //    {
                //        ServerCode(code);
                //    }
                //    throw new NntpException(response);
                //}

                // System.Console.WriteLine(response);

                char[] sep = { '\n' };

                while (true)
                {
                    response = Response();
                    string[] newsgroups = response.Split(sep);

                    foreach (string group in newsgroups)
                    {
                        if (group.Length > 2)
                        {
                            char[] groupseps = { ' ' };
                            //retval.Add(group);
                            retval.Add(group.Split(groupseps)[0]);
                            //System.Console.WriteLine(group);
                        }
                    }
                    if (response.Substring(response.Length - 3, 3) == ".\r\n" ||
                        response.Substring(response.Length - 2, 2) == ".\n")
                    {
                        break;
                    }
                }
                retval.Sort();
                return retval;
            }
            catch (System.Exception e)
            {
                System.Console.WriteLine("Exception in Getnewsgroups");
                System.Console.WriteLine(e.Message);
                throw e;
            }
        }

        /// <summary>
        /// Performs an Xover in a newsgroup to retrieve list of headers in range
        /// </summary>
        /// <param name="newsgroup">Newsgroup</param>
        /// <param name="start">First message ID to retrieve</param>
        /// <param name="end">Last message ID to retrieve</param>
        /// <returns>List of type string of headers in newsgroup</returns>
        public List<string> Xover(string newsgroup, long start, long end)
        {
            string command = "GROUP " + newsgroup;
            string response = string.Empty;
            StringBuilder sb = new StringBuilder();
            string code = string.Empty;
            List<string> retVal = new List<String>();

            try
            {
                Write(command);
                response = Response();

                sb.Append("XOVER ");
                sb.Append(start);
                sb.Append("-");
                sb.Append(end);
                command = sb.ToString();
                System.Console.WriteLine(command);
                Write(command);
                response = Response();
                //System.Console.WriteLine(response);
                code = response.Substring(0, 3);

                switch (code)
                {
                    case "224":
                        char[] seps = { '\n' };

                        while (true)
                        {
                            string[] headers = response.Split(seps);
                            for (int i = 0; i < headers.Length; i++)
                            {
                                if (headers[i].Length > 0)
                                {
                                    //System.Console.WriteLine("{0}: {1}", i, headers[i]);
                                    retVal.Add(headers[i]);
                                }
                            }
                            if (response.Substring(response.Length - 3, 3) == ".\r\n" ||
                                response.Substring(response.Length - 2, 2) == ".\n")
                                break;
                            response = Response();
                        }
                        break;
                    default:
                        throw new NntpException(Int32.Parse(response.Substring(0, 3)), response);
                }
            }
            catch (System.ArgumentOutOfRangeException aore)
            {
                System.Console.WriteLine(aore.Message);
                throw aore;
            }

            return retVal;
        }

        /// <summary>
        /// Gets Group information in array of two Int64:
        /// start and end of newsgroup.
        /// </summary>
        /// <param name="newsgroup">Newsgroup to perform GROUP on</param>
        /// <returns>Int64[2] of start and end messages of newsgroup</returns>
        public Int64[] Group(string newsgroup)
        {
            Int64[] retVal = new Int64[2];
            string command = "GROUP " + newsgroup;
            string response = string.Empty;
            string code = string.Empty;

            Write(command);
            response = Response();
            this.SERVERMESSAGE = response;

            try
            {
                code = response.Substring(0, 3);

                switch (code)
                {
                    case "211":
                        {
                            char[] seps = { ' ' };


                            string[] values = response.Split(seps);
                            retVal[0] = Int64.Parse(values[2]);
                            retVal[1] = Int64.Parse(values[3]);
                            

                            //for (int i = 0; i < 4; i++)
                            //{
                            //    System.Console.WriteLine("Value at {0}: {1}", i, values[i]);
                            //}
                        }
                        break;
                    default:
                        throw new NntpException(Int32.Parse(code), response);
                }
            }
            catch (Exception e)
            {
                System.Console.WriteLine("Exception occurred in Group");
                throw e;
            }

            return retVal;
        }
        /// <summary>
        /// Retrives the header of message messageid.
        /// </summary>
        /// <param name="newsgroup">Newsgroup which messageid belongs to.</param>
        /// <param name="messageid">Messageid to perform HEAD on.</param>
        /// <returns></returns>
        public string Header(string newsgroup, string messageid)
        {
            string retVal = string.Empty;
            string command = "HEAD " + messageid;
            string response = string.Empty;
            string code = string.Empty;

            try
            {
                Write(command);
                response = Response();
                code = response.Substring(0, 3);

                switch (code)
                {
                    case "221":
                        while (true)
                        {
                            retVal += response;

                            if (response.Substring(response.Length - 3, 3) == ".\r\n" ||
                                response.Substring(response.Length - 2, 2) == ".\n")
                                break;
                            response = Response();
                        }
                        break;
                    default:
                        throw new NntpException(Int32.Parse(code), response);
                }
            }
            catch (Exception e)
            {
                System.Console.WriteLine("Exception in Body");
                System.Console.WriteLine(e.Message);
                throw e;
            }
            //System.Console.WriteLine(retVal);
            return retVal;
        }
        /// <summary>
        /// Retrieves the body of message messageid.
        /// </summary>
        /// <param name="newsgroup">Newsgroup which messageid belongs to.</param>
        /// <param name="messageid">Messageid to perform BODY on.</param>
        /// <returns></returns>
        public string[] Body(string newsgroup, string messageid)
        {
            string[] retVal;
            string body;
            string command = "BODY " + messageid;
            string response = string.Empty;
            string code = string.Empty;

            //System.IO.FileInfo file = new System.IO.FileInfo(@"C:\Users\Timothy Roberts\Documents\Visual Studio 2008\Projects\NntpSharp\testfiles\file2.ntx");
            //if (file.Exists)
            //    file.Delete();

            //System.IO.StreamWriter sw = file.AppendText();
            //System.IO.StreamWriter sw = new System.IO.StreamWriter(@"C:\Users\Timothy Roberts\Documents\Visual Studio 2008\Projects\NntpSharp\testfiles\file2.ntx", true, Encoding.ASCII);

            try
            {
                Write(command);
                response = Response();
                code = response.Substring(0, 3);

                response = response.Substring(response.IndexOf('\n'));
                response = response.Substring(response.IndexOf('\n'));
                this.SERVERMESSAGE = response.Substring(0, response.IndexOf('\n'));

                switch (code)
                {
                    case "222":
                        while (true)
                        {
                            body += response;
                            //sw.WriteLine("First line is: {0}", this.SERVERMESSAGE);
                            //sw.Write(response);

                            if (response.Substring(response.Length - 3, 3) == ".\r\n" ||
                                response.Substring(response.Length - 2, 2) == ".\n")
                            {
                                //sw.Close();
                                break;
                            }
                            response = Response();
                        }
                        break;
                    default:
                        throw new NntpException(Int32.Parse(code), response);
                }
            }
            catch (Exception e)
            {
                System.Console.WriteLine("Exception in Body");
                //System.Console.WriteLine(e.Message);
                throw e;
            }
            //Console.WriteLine(retVal);
            retVal = body.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
            return retVal;
        }

        /* TEMPLATE
        public string Body(string newsgroup, long messageid)
        {
            string retVal = string.Empty;
            string command = "BODY " + messageid.ToString();
            string response = string.Empty;
            string code = string.Empty;

            try
            {
                Write(command);
                response = Response();
                code = response.Substring(0,3);
               
                switch (code)
                {
                    case "222":
                        while (true)
                        {
                            retVal += response;

                            if (response.Substring(response.Length - 3, 3) == ".\r\n" ||
                                response.Substring(response.Length - 2, 2) == ".\n")
                                break;
                            response = Response();
                        }
                        break;
                    default:
                        throw new NntpException(Int32.Parse(code), response);
                }
            }
            catch (Exception e)
            {
                System.Console.WriteLine("Exception in Body");
                System.Console.WriteLine(e.Message);
                throw e;
            }
            return retVal;
        }
        */
        /// <summary>
        /// Posts a message to a newsgroup.
        /// </summary>
        /// <param name="newsgroup">Newsgroup to POST to.</param>
        /// <param name="subject">Subject of POST.</param>
        /// <param name="from">Handle attached to POST.</param>
        /// <param name="content">Content of POST.</param>
        public void Post(string newsgroup, string subject, string from, string content)
        {
            string command = "POST " + newsgroup;
            string response = string.Empty;
            string code = string.Empty;

            try
            {
                Write(command);
                response = Response();
                code = response.Substring(0, 3);

                switch (code)
                {
                    case "340":
                        command = "From: " + from + "\r\n"
                            + "Newsgroups: " + newsgroup + "\r\n"
                            + "Subject: " + subject + "\r\n\r\n"
                            + content + "\r\n.\r\n";

                        Write(command);
                        response = Response();
                        code = response.Substring(0, 3);

                        switch (code)
                        {
                            case "240":
                                return;
                            default:
                                throw new NntpException(Int32.Parse(code), response);
                        }
                    default:
                        throw new NntpException(Int32.Parse(code), response);
                }
            }
            catch (Exception e)
            {
                System.Console.WriteLine("Exception in Body");
                System.Console.WriteLine(e.Message);
                throw e;
            }
        }



        /// <summary>
        /// Writes a command to the news server
        /// </summary>
        /// <param name="message">NNTP command</param>
        private void Write(string message)
        {
            System.Text.ASCIIEncoding en = new System.Text.ASCIIEncoding();

            if (message.Substring(message.Length - 2, 2) != "\r\n")
                message += "\r\n";
            else if (message.Substring(message.Length - 1, 1) != "\n")
                message = message.Substring(0, message.Length - 1) + "\r\n";

            if (message.Length > 1024)
                throw new NntpException("Message is too long to write to server.\r\n");

            byte[] WriteBuffer = new byte[message.Length];

            try
            {
                WriteBuffer = en.GetBytes(message);

                NetworkStream stream = GetStream();
                stream.Write(WriteBuffer, 0, WriteBuffer.Length);

                Debug.WriteLine("WRITE:" + message);
            }
            catch (System.ArgumentOutOfRangeException aore)
            {
                System.Console.WriteLine(aore.Message);
                this.SERVERMESSAGE = aore.Message;
                //throw new NntpException(aore.Message);
            }
            catch (System.ArgumentNullException ane)
            {
                System.Console.WriteLine(ane.Message);
                this.SERVERMESSAGE = ane.Message;
                //throw new NntpException(ane.Message);
            }
            catch (System.ArgumentException ae)
            {
                System.Console.WriteLine(ae.Message);
                this.SERVERMESSAGE = ae.Message;
                //throw new NntpException(ae.Message);
            }
        }

        /// <summary>
        /// Reads command response from server.
        /// </summary>
        /// <returns>Response from server</returns>
        private string Response()
        {
            System.Text.ASCIIEncoding enc = new ASCIIEncoding();
            byte[] serverbuff = new Byte[1024];
            //int count = 0;
            int bufferLength = 0;
            string retVal = string.Empty;

            //Read one megabyte at a time from server.
            //Continue reading until we hit the last two chars as "\r\n"
            try
            {
                NetworkStream stream = GetStream();
                while ((bufferLength = stream.Read(serverbuff, 0, serverbuff.Length)) > 0)
                {
                    retVal += enc.GetString(serverbuff, 0, bufferLength);
                    if (retVal.Substring(retVal.Length - 2, 2) == "\r\n" ||
                        retVal.Substring(retVal.Length - 3, 3) == ".\r\n" ||
                        retVal.Length == 0)
                    {
                        //We are at the end of the server message. Quit reading.
                        break;
                    }
                }
                Debug.WriteLine("READ:" + retVal);
            }
            catch (System.ArgumentNullException ane)
            {
                System.Console.WriteLine("Exception in Response");
                System.Console.WriteLine(ane.Message);
                throw ane;
            }
            catch (System.ArgumentOutOfRangeException aore)
            {
                System.Console.WriteLine("Exception in Response");
                System.Console.WriteLine(aore.Message);
                throw aore;
            }
            catch (System.InvalidOperationException ioe)
            {
                System.Console.WriteLine("Exception in Response");
                System.Console.WriteLine(ioe.Message);
                throw ioe;
            }

            return retVal;
        }
    }
}